#include <fmt/format.h>
#include "../opengl/opengl.hpp"
#include "../gl/Camera.hpp"
#include "../gl/Sphere.hpp"
#include "../gl/Window.hpp"

namespace gl = opengl;

struct AxisHelper {
    static constexpr auto vs_src =
        "#version 450 core\n"
        "layout(location=0)in vec3 pos;layout(location=0)out vec4 color;"
        "struct World{vec3 cameraPos;mat4 matVP;};layout(binding=0,std140)uniform WorldBlock{World world;};"
        "void main(){gl_Position=world.matVP*vec4(pos,1.0);const vec3 colors[]={vec3(1,0,0),vec3(0,1,0),vec3(0,0,1)};"
        "color=vec4(colors[gl_VertexID>>1],1.0);}";
    static constexpr auto fs_src =
        "#version 450 core\n"
        "layout(location=0)in vec4 color;layout(location=0)out vec4 fragColor;void main(){fragColor=color;}";

    struct {
        glm::vec3 x1{0.0}, x2{0.0}; // Red
        glm::vec3 y1{0.0}, y2{0.0}; // Green
        glm::vec3 z1{0.0}, z2{0.0}; // Blue
    } data;
    gl::buffer vertices;
    gl::vertex_array vao;
    gl::program program;

    AxisHelper(float size = 1.0f)
        : vertices(sizeof(data)), vao({gl::vertex_attribute(0, gl::type_f32, 3, 0)}),
          program({gl::shader(gl::shader::vertex_shader, vs_src), gl::shader(gl::shader::fragment_shader, fs_src)}) {
        auto half = size / 2.0;
        data.x1.x = -half;
        data.x2.x = half;
        data.y1.y = -half;
        data.y2.y = half;
        data.z1.z = -half;
        data.z2.z = half;
        vertices.set_data(&data, sizeof(data));
    }
    void render() const {
        vao.bind();
        program.use();
        vao.set_vertex_buffer(vertices, sizeof(glm::vec3));
        gl::api::glDrawArrays(gl::lines, 0, 6);
    }
};

template <> struct fmt::formatter<glm::vec3> {
    constexpr auto parse(format_parse_context &ctx) const { return ctx.begin(); }
    auto format(const glm::vec3 &v, format_context &ctx) const {
        return fmt::format_to(ctx.out(), "({},{},{})", v.x, v.y, v.z);
    }
};

int main() {
    Window window("Model", 800, 600);
    gl::init_opengl(gl::opengl_config{.load_function = &SDL_GL_GetProcAddress,
                                      .debug_msg_callback = [](auto, auto, auto id, auto, auto, auto msg, auto) {
                                          fmt::println("[OpenGL object {}] {}", id, msg);
                                      }});
    AxisHelper helper(5.0);

    gl::vertex_array vao({
        gl::vertex_attribute(0, gl::type_f32, 3, 0),  // position
        gl::vertex_attribute(1, gl::type_f32, 3, 12), // normal
    });
    Sphere<64> mesh(1.0f);
    gl::buffer vertices(sizeof(mesh.vertices), mesh.vertices);
    gl::buffer indices(sizeof(mesh.indices), mesh.indices);
    gl::program program({gl::shader::from_file(gl::shader::vertex_shader, "../shader/multilight/vs.glsl"),
                         gl::shader::from_file(gl::shader::fragment_shader, "../shader/multilight/fs.glsl")});
    fmt::println("{}", program.get_error());

    PerspectiveCamera camera;
    camera.set_position({4.0f, 4.0f, -4.0f});
    camera.look_at(glm::vec3{0.0f});

    window.on_exec = [&](auto) {
        auto [w, h] = window.get_size();
        gl::api::glViewport(0, 0, w, h);
        camera.set_aspect_ratio((float) w / (float) h);
    };
    window.on_resize = [&](auto, int w, int h) {
        gl::api::glViewport(0, 0, w, h);
        camera.set_aspect_ratio((float) w / (float) h);
    };

    // cameraPos: vec3 (0-16) matVP: mat4 (16-80)
    gl::buffer worldUBO(80); // binding = 0
    worldUBO.bind_to(gl::buffer::uniform_buffer, 0);
    constexpr gl::u32 NUM_MAX_LIGHTS = 8;
    gl::u32 numLights = 4;
    // size: 64bytes
    struct alignas(16) Light {
        glm::vec3 ambient{0.1f, 0.1f, 0.1f};
        gl::f32 __pad0__;
        glm::vec3 diffuse{0.6f, 0.6f, 0.6f};
        gl::f32 __pad1__;
        glm::vec3 specular{0.3f, 0.3f, 0.3f};
        gl::f32 __pad2__;
        glm::vec3 position{10.0f, 10.0f, 0.0f};
        gl::f32 __pad3__;
    } light[NUM_MAX_LIGHTS];
    gl::buffer lightUBO(sizeof(Light) * NUM_MAX_LIGHTS); // binding = 1
    lightUBO.bind_to(gl::buffer::uniform_buffer, 1);
    for (gl::u32 i = 0; i < numLights; ++i) {
        light[i].position.z = i;
    }

    struct alignas(16) Transform {
        glm::mat4 modelMatrix;
        glm::mat4 mvpMatrix;
        glm::mat4 normalMatrix;
    } transform; // location = 0-2
    struct Material {
        glm::vec3 color{1.0f};
        float shiness = 16.0f;
    } material; // location = 3-4
    // numLights: location=5

    window.on_render = [&](auto) {
        gl::clear_buffers();
        helper.render();
        vao.bind();
        program.use();
        // World
        worldUBO.set_data(glm::value_ptr(camera.position), sizeof(glm::vec3));
        worldUBO.set_data(glm::value_ptr(camera.viewProjectionMatrix), sizeof(glm::mat4), 16);
        // Light
        lightUBO.set_data(&light, sizeof(Light) * numLights);
        // Transform
        program.set_mat4f(1, glm::value_ptr(camera.viewProjectionMatrix)); // mvpMatrix;
        // Material
        program.set_vec3f(3, glm::value_ptr(material.color));
        program.set_float32(4, material.shiness);
        // numLights
        program.set_uint32(5, numLights);

        vao.set_vertex_buffer(vertices, sizeof(Sphere<64>::vertex));
        vao.set_index_buffer(indices);
        gl::api::glDrawElements(gl::triangles, Sphere<64>::num_indices, gl::type_u32, nullptr);
    };
    window.on_process_event = [&](auto, const SDL_Event *event) {
        if (event->type == SDL_EventType::SDL_MOUSEWHEEL) {
            constexpr float sensitivity = 2.0f;
            auto fov = glm::clamp(camera.fov - sensitivity * event->wheel.preciseY, 15.0f, 85.0f);
            camera.set_fov(fov);
        }
    };

    return window.exec();
}